﻿using System;
using GameAnalyticsSDK.Net.Logging;
using GameAnalyticsSDK.Net.Utilities;
using GameAnalyticsSDK.Net.State;
using GameAnalyticsSDK.Net.Http;

namespace GameAnalyticsSDK.Net.Validators
{
	internal static class GAValidator
	{
		#region Public methods

		public static bool ValidateBusinessEvent(string currency, long amount, string cartType, string itemType, string itemId)
		{
			// validate currency
			if (!ValidateCurrency(currency))
			{
				GALogger.W("Validation fail - business event - currency: Cannot be (null) and need to be A-Z, 3 characters and in the standard at openexchangerates.org. Failed currency: " + currency);
				return false;
			}

			if (amount < 0)
            {
                GALogger.W("Validation fail - business event - amount. Cannot be less than 0. Failed amount: " + amount);
                return false;
            }

			// validate cartType
			if (!ValidateShortString(cartType, true))
			{
				GALogger.W("Validation fail - business event - cartType. Cannot be above 32 length. String: " + cartType);
				return false;
			}

			// validate itemType length
			if (!ValidateEventPartLength(itemType, false))
			{
				GALogger.W("Validation fail - business event - itemType: Cannot be (null), empty or above 64 characters. String: " + itemType);
				return false;
			}

			// validate itemType chars
			if (!ValidateEventPartCharacters(itemType))
			{
				GALogger.W("Validation fail - business event - itemType: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + itemType);
				return false;
			}

			// validate itemId
			if (!ValidateEventPartLength(itemId, false))
			{
				GALogger.W("Validation fail - business event - itemId. Cannot be (null), empty or above 64 characters. String: " + itemId);
				return false;
			}

			if (!ValidateEventPartCharacters(itemId))
			{
				GALogger.W("Validation fail - business event - itemId: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + itemId);
				return false;
			}

			return true;
		}

		public static bool ValidateResourceEvent(EGAResourceFlowType flowType, string currency, long amount, string itemType, string itemId)
		{
			if (flowType == EGAResourceFlowType.Undefined)
			{
				GALogger.W("Validation fail - resource event - flowType: Invalid flow type.");
				return false;
			}
			if (string.IsNullOrEmpty(currency))
			{
				GALogger.W("Validation fail - resource event - currency: Cannot be (null)");
				return false;
			}
			if (!GAState.HasAvailableResourceCurrency(currency))
			{
				GALogger.W("Validation fail - resource event - currency: Not found in list of pre-defined available resource currencies. String: " + currency);
				return false;
			}
			if (!(amount > 0))
			{
				GALogger.W("Validation fail - resource event - amount: Float amount cannot be 0 or negative. Value: " + amount);
				return false;
			}
			if (string.IsNullOrEmpty(itemType))
			{
				GALogger.W("Validation fail - resource event - itemType: Cannot be (null)");
				return false;
			}
			if (!ValidateEventPartLength(itemType, false))
			{
				GALogger.W("Validation fail - resource event - itemType: Cannot be (null), empty or above 64 characters. String: " + itemType);
				return false;
			}
			if (!ValidateEventPartCharacters(itemType))
			{
				GALogger.W("Validation fail - resource event - itemType: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + itemType);
				return false;
			}
			if (!GAState.HasAvailableResourceItemType(itemType))
			{
				GALogger.W("Validation fail - resource event - itemType: Not found in list of pre-defined available resource itemTypes. String: " + itemType);
				return false;
			}
			if (!ValidateEventPartLength(itemId, false))
			{
				GALogger.W("Validation fail - resource event - itemId: Cannot be (null), empty or above 64 characters. String: " + itemId);
				return false;
			}
			if (!ValidateEventPartCharacters(itemId))
			{
				GALogger.W("Validation fail - resource event - itemId: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + itemId);
				return false;
			}
			return true;
		}

		public static bool ValidateProgressionEvent(EGAProgressionStatus progressionStatus, string progression01, string progression02, string progression03)
		{
			if (progressionStatus == EGAProgressionStatus.Undefined)
			{
				GALogger.W("Validation fail - progression event: Invalid progression status.");
				return false;
			}

			// Make sure progressions are defined as either 01, 01+02 or 01+02+03
			if (!string.IsNullOrEmpty(progression03) && !(!string.IsNullOrEmpty(progression02) || string.IsNullOrEmpty(progression01)))
			{
				GALogger.W("Validation fail - progression event: 03 found but 01+02 are invalid. Progression must be set as either 01, 01+02 or 01+02+03.");
				return false;
			}
			else if (!string.IsNullOrEmpty(progression02) && string.IsNullOrEmpty(progression01))
			{
				GALogger.W("Validation fail - progression event: 02 found but not 01. Progression must be set as either 01, 01+02 or 01+02+03");
				return false;
			}
			else if (string.IsNullOrEmpty(progression01))
			{
				GALogger.W("Validation fail - progression event: progression01 not valid. Progressions must be set as either 01, 01+02 or 01+02+03");
				return false;
			}

			// progression01 (required)
			if (!ValidateEventPartLength(progression01, false))
			{
				GALogger.W("Validation fail - progression event - progression01: Cannot be (null), empty or above 64 characters. String: " + progression01);
				return false;
			}
			if (!ValidateEventPartCharacters(progression01))
			{
				GALogger.W("Validation fail - progression event - progression01: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + progression01);
				return false;
			}
			// progression02
			if (!string.IsNullOrEmpty(progression02))
			{
				if (!ValidateEventPartLength(progression02, true))
				{
					GALogger.W("Validation fail - progression event - progression02: Cannot be empty or above 64 characters. String: " + progression02);
					return false;
				}
				if (!ValidateEventPartCharacters(progression02))
				{
					GALogger.W("Validation fail - progression event - progression02: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + progression02);
					return false;
				}
			}
			// progression03
			if (!string.IsNullOrEmpty(progression03))
			{
				if (!ValidateEventPartLength(progression03, true))
				{
					GALogger.W("Validation fail - progression event - progression03: Cannot be empty or above 64 characters. String: " + progression03);
					return false;
				}
				if (!ValidateEventPartCharacters(progression03))
				{
					GALogger.W("Validation fail - progression event - progression03: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + progression03);
					return false;
				}
			}
			return true;
		}

		public static bool ValidateDesignEvent(string eventId, double value)
		{
			if (!ValidateEventIdLength(eventId))
			{
				GALogger.W("Validation fail - design event - eventId: Cannot be (null) or empty. Only 5 event parts allowed seperated by :. Each part need to be 32 characters or less. String: " + eventId);
				return false;
			}
			if (!ValidateEventIdCharacters(eventId))
			{
				GALogger.W("Validation fail - design event - eventId: Non valid characters. Only allowed A-z, 0-9, -_., ()!?. String: " + eventId);
				return false;
			}
			// value: allow 0, negative and nil (not required)
			return true;
		}

		public static bool ValidateErrorEvent(EGAErrorSeverity severity, string message)
		{
			if (severity == EGAErrorSeverity.Undefined)
			{
				GALogger.W("Validation fail - error event - severity: Severity was unsupported value.");
				return false;
			}
			if (!ValidateLongString(message, true))
			{
				GALogger.W("Validation fail - error event - message: Message cannot be above 8192 characters.");
				return false;
			}
			return true;
		}

		public static bool ValidateSdkErrorEvent(string gameKey, string gameSecret, EGASdkErrorType type)
		{
			if(!ValidateKeys(gameKey, gameSecret))
			{
				return false;
			}

			if (type == EGASdkErrorType.Undefined)
			{
				GALogger.W("Validation fail - sdk error event - type: Type was unsupported value.");
				return false;
			}
			return true;
		}

		public static bool ValidateKeys(string gameKey, string gameSecret)
		{
			if (GAUtilities.StringMatch(gameKey, "^[A-z0-9]{32}$"))
			{
				if (GAUtilities.StringMatch(gameSecret, "^[A-z0-9]{40}$"))
				{
					return true;
				}
			}
			return false;
		}

		public static bool ValidateCurrency(string currency)
		{
			if (string.IsNullOrEmpty(currency))
			{
				return false;
			}
			if (!GAUtilities.StringMatch(currency, "^[A-Z]{3}$"))
			{
				return false;
			}
			return true;
		}

		public static bool ValidateEventPartLength(string eventPart, bool allowNull)
		{
			if (allowNull == true && string.IsNullOrEmpty(eventPart))
			{
				return true;
			}

			if (string.IsNullOrEmpty(eventPart))
			{
				return false;
			}

			if (eventPart.Length > 64)
			{
				return false;
			}
			return true;
		}

		public static bool ValidateEventPartCharacters(string eventPart)
		{
			if (!GAUtilities.StringMatch(eventPart, "^[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}$"))
			{
				return false;
			}
			return true;
		}

		public static bool ValidateEventIdLength(string eventId)
		{
			if (string.IsNullOrEmpty(eventId))
			{
				return false;
			}

			if (!GAUtilities.StringMatch(eventId, "^[^:]{1,64}(?::[^:]{1,64}){0,4}$"))
			{
				return false;
			}
			return true;
		}

		public static bool ValidateEventIdCharacters(string eventId)
		{
			if (string.IsNullOrEmpty(eventId))
			{
				return false;
			}

			if (!GAUtilities.StringMatch(eventId, "^[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}(:[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}){0,4}$"))
			{
				return false;
			}
			return true;
		}

		public static JSONObject ValidateAndCleanInitRequestResponse(JSONNode initResponse)
		{
			// make sure we have a valid dict
			if (initResponse == null)
			{
				GALogger.W("validateInitRequestResponse failed - no response dictionary.");
				return null;
			}

			JSONObject validatedDict = new JSONObject();

			// validate enabled field
			try
			{
				validatedDict.Add("enabled", new JSONBool(initResponse["enabled"].IsBoolean ? initResponse["enabled"].AsBool : true));
			}
			catch (Exception)
			{
				GALogger.W("validateInitRequestResponse failed - invalid type in 'enabled' field.");
				return null;
			}

			// validate server_ts
			try
			{
				long serverTsNumber = initResponse["server_ts"].IsNumber ? initResponse["server_ts"].AsLong : -1;
				if (serverTsNumber > 0)
				{
					validatedDict.Add("server_ts", new JSONNumber(serverTsNumber));
				}
			}
			catch (Exception e)
			{
				GALogger.W("validateInitRequestResponse failed - invalid type in 'server_ts' field. type=" + initResponse["server_ts"].GetType() + ", value=" + initResponse["server_ts"] + ", " + e);
				return null;
			}

			// validate configurations field
			try
			{
				validatedDict.Add("configurations", initResponse["configurations"].IsArray ? initResponse["configurations"].AsArray : new JSONArray());
			}
			catch (Exception e)
			{
				GALogger.W("validateInitRequestResponse failed - invalid type in 'configurations' field. type=" + initResponse["configurations"].GetType() + ", value=" + initResponse["configurations"] + ", " + e);
				return null;
			}

			return validatedDict;
		}

		public static bool ValidateBuild(string build)
		{
			if (!ValidateShortString(build, false))
			{
				return false;
			}
			return true;
		}

		public static bool ValidateSdkWrapperVersion(string wrapperVersion)
		{
			if (!GAUtilities.StringMatch(wrapperVersion, "^(unity) [0-9]{0,5}(\\.[0-9]{0,5}){0,2}$"))
			{
				return false;
			}
			return true;
		}

		public static bool ValidateEngineVersion(string engineVersion)
		{
			if (engineVersion == null || !GAUtilities.StringMatch(engineVersion, "^(unity) [0-9]{0,5}(\\.[0-9]{0,5}){0,2}$"))
			{
				return false;
			}
			return true;
		}

		public static bool ValidateUserId(string uId)
		{
			if (!ValidateString(uId, false))
			{
				GALogger.W("Validation fail - user id: id cannot be (null), empty or above 64 characters.");
				return false;
			}
			return true;
		}

		public static bool ValidateShortString(string shortString, bool canBeEmpty)
		{
			// String is allowed to be empty or nil
			if (canBeEmpty && string.IsNullOrEmpty(shortString))
			{
				return true;
			}

			if (string.IsNullOrEmpty(shortString) || shortString.Length > 32)
			{
				return false;
			}
			return true;
		}

		public static bool ValidateString(string s, bool canBeEmpty)
		{
			// String is allowed to be empty or nil
			if (canBeEmpty && string.IsNullOrEmpty(s))
			{
				return true;
			}

			if (string.IsNullOrEmpty(s) || s.Length > 64)
			{
				return false;
			}
			return true;
		}

		public static bool ValidateLongString(string longString, bool canBeEmpty)
		{
			// String is allowed to be empty
			if (canBeEmpty && string.IsNullOrEmpty(longString))
			{
				return true;
			}

			if (string.IsNullOrEmpty(longString) || longString.Length > 8192)
			{
				return false;
			}
			return true;
		}

		public static bool ValidateConnectionType(string connectionType)
		{
			return GAUtilities.StringMatch(connectionType, "^(wwan|wifi|lan|offline)$");
		}

		public static bool ValidateCustomDimensions(params string[] customDimensions)
		{
			return ValidateArrayOfStrings(20, 32, false, "custom dimensions", customDimensions);
		}

		public static bool ValidateResourceCurrencies(params string[] resourceCurrencies)
		{
			if (!ValidateArrayOfStrings(20, 64, false, "resource currencies", resourceCurrencies))
			{
				return false;
			}

			// validate each string for regex
			foreach (string resourceCurrency in resourceCurrencies)
			{
				if (!GAUtilities.StringMatch(resourceCurrency, "^[A-Za-z]+$"))
				{
					GALogger.W("resource currencies validation failed: a resource currency can only be A-Z, a-z. String was: " + resourceCurrency);
					return false;
				}
			}
			return true;
		}

		public static bool ValidateResourceItemTypes(params string[] resourceItemTypes)
		{
			if (!ValidateArrayOfStrings(20, 32, false, "resource item types", resourceItemTypes))
			{
				return false;
			}

			// validate each resourceItemType for eventpart validation
			foreach (string resourceItemType in resourceItemTypes)
			{
				if (!ValidateEventPartCharacters(resourceItemType))
				{
					GALogger.W("resource item types validation failed: a resource item type cannot contain other characters than A-z, 0-9, -_., ()!?. String was: " + resourceItemType);
					return false;
				}
			}
			return true;
		}

		public static bool ValidateDimension01(string dimension01)
		{
			// allow nil
			if (string.IsNullOrEmpty(dimension01))
			{
				return true;
			}
			if (!GAState.HasAvailableCustomDimensions01(dimension01))
			{
				return false;
			}
			return true;
		}

		public static bool ValidateDimension02(string dimension02)
		{
			// allow nil
			if (string.IsNullOrEmpty(dimension02))
			{
				return true;
			}
			if (!GAState.HasAvailableCustomDimensions02(dimension02))
			{
				return false;
			}
			return true;
		}

		public static bool ValidateDimension03(string dimension03)
		{
			// allow nil
			if (string.IsNullOrEmpty(dimension03))
			{
				return true;
			}
			if (!GAState.HasAvailableCustomDimensions03(dimension03))
			{
				return false;
			}
			return true;
		}

		public static bool ValidateArrayOfStrings(long maxCount, long maxStringLength, bool allowNoValues, string logTag, params string[] arrayOfStrings)
		{
			string arrayTag = logTag;

			// use arrayTag to annotate warning log
			if (string.IsNullOrEmpty(arrayTag))
			{
				arrayTag = "Array";
			}

			if(arrayOfStrings == null)
			{
				GALogger.W(arrayTag + " validation failed: array cannot be null. ");
				return false;
			}

			// check if empty
			if (allowNoValues == false && arrayOfStrings.Length == 0)
			{
				GALogger.W(arrayTag + " validation failed: array cannot be empty. ");
				return false;
			}

			// check if exceeding max count
			if (maxCount > 0 && arrayOfStrings.Length > maxCount)
			{
				GALogger.W(arrayTag + " validation failed: array cannot exceed " + maxCount + " values. It has " + arrayOfStrings.Length + " values.");
				return false;
			}

			// validate each string
			foreach (string arrayString in arrayOfStrings)
			{
				int stringLength = arrayString == null ? 0 : arrayString.Length;
				// check if empty (not allowed)
				if (stringLength == 0)
				{
					GALogger.W(arrayTag + " validation failed: contained an empty string.");
					return false;
				}

				// check if exceeding max length
				if (maxStringLength > 0 && stringLength > maxStringLength)
				{
					GALogger.W(arrayTag + " validation failed: a string exceeded max allowed length (which is: " + maxStringLength + "). String was: " + arrayString);
					return false;
				}
			}
			return true;
		}

		public static bool ValidateFacebookId(string facebookId)
		{
			if (!ValidateString(facebookId, false))
			{
				GALogger.W("Validation fail - facebook id: id cannot be (null), empty or above 64 characters.");
				return false;
			}
			return true;
		}

		public static bool ValidateGender(EGAGender gender)
		{
			if (gender == EGAGender.Undefined || !(gender == EGAGender.Male || gender == EGAGender.Female))
			{
				GALogger.W("Validation fail - gender: Has to be 'male' or 'female'.");
				return false;
			}
			return true;
		}

		public static bool ValidateBirthyear(long birthYear)
		{
			if (birthYear < 0 || birthYear > 9999)
			{
				GALogger.W("Validation fail - birthYear: Cannot be (null) or invalid range.");
				return false;
			}
			return true;
		}

		public static bool ValidateClientTs(long clientTs)
		{
			// TODO(nikolaj): validate other way? (instead of max possible)
			if (clientTs < (long.MinValue+1) || clientTs > (long.MaxValue-1))
			{
				return false;
			}
			return true;
		}

		#endregion // Public methods
	}
}

